package net.shambolica.jport.runner;

import com.ericsson.otp.erlang.OtpErlangAtom;
import com.ericsson.otp.erlang.OtpErlangBinary;
import com.ericsson.otp.erlang.OtpErlangDecodeException;
import com.ericsson.otp.erlang.OtpErlangDouble;
import com.ericsson.otp.erlang.OtpErlangList;
import com.ericsson.otp.erlang.OtpErlangLong;
import com.ericsson.otp.erlang.OtpErlangObject;
import com.ericsson.otp.erlang.OtpErlangRef;
import com.ericsson.otp.erlang.OtpErlangString;
import com.ericsson.otp.erlang.OtpErlangTuple;
import com.ericsson.otp.erlang.OtpInputStream;
import com.ericsson.otp.erlang.OtpOutputStream;
import com.ericsson.otp.erlang.OtpExternal;
import net.erl.objects.ET;
import net.erl.objects.ErlAtom;
import net.erl.objects.ErlBinary;
import net.erl.objects.ErlFloat;
import net.erl.objects.ErlInteger;
import net.erl.objects.ErlList;
import net.erl.objects.ErlRef;
import net.erl.objects.ErlTerm;
import net.erl.objects.ErlTuple;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ProtocolException;
import java.util.Iterator;

/**
 * Stream of ETF (binary Erlang representation).
 * This uses the JInterface classes to implement the wire protocol.
 */
public class EtfStreamConnection extends SimpleStreamConnection {

    public EtfStreamConnection(InputStream input, OutputStream output) {
        super(input, output, false);
    }

    public ErlTerm parseTerm(byte[] buf) throws IOException {
        OtpInputStream in = new OtpInputStream(buf);
        OtpErlangObject otpTerm;

        // Decode Otp Object.
        try {
            otpTerm = in.read_any();
        } catch (OtpErlangDecodeException e) {
            log.error("erlang decode exception: {}", e.getMessage());
            throw new IOException("erlang decode exception "+e.getMessage());
        }

        return otpToErl(otpTerm, buf);
    }


    public byte[] serializeTerm(ErlTerm term) throws IOException {
        try {
            OtpErlangObject otpTerm = erlToOtp(term);
            return otpToBytes(otpTerm);
        } catch (OtpErlangDecodeException e) {
            throw new IOException("erlang decode exception: "+e.getMessage());
        }
    }


    public OtpErlangObject erlToOtp(ErlTerm term) throws IOException, OtpErlangDecodeException {
        if (term.isAtom()) {
            return new OtpErlangAtom(((ErlAtom)term).getValue());
        } else if (term.isBinary()) {
            return new OtpErlangBinary(((ErlBinary)term).getBuffer(false));
        } else if (term.isFloat()) {
            return new OtpErlangDouble(((ErlFloat)term).getFloatValue());
        } else if (term.isInteger()) {
            return new OtpErlangLong(((ErlInteger)term).getLongValue());
        } else if (term.isList()) {
            return new OtpErlangList(erlToOtp(term.size(), ((ErlList)term).iterator()));
        } else if(term.isRef()) {
            return new OtpErlangRef(new OtpInputStream(((ErlRef)term).getBufferRepresentation(false)));
        } else if (term.isTuple()) {
            return new OtpErlangTuple(erlToOtp(term.size(), ((ErlTuple)term).iterator()));
        } else {
            log.error("unhandled Erl term type: {}", term);
            throw new ProtocolException("unhandled Erl term type: "+term);
        }

    }


    private OtpErlangObject[] erlToOtp(int nElements, Iterator<ErlTerm> it) throws IOException, OtpErlangDecodeException {
        OtpErlangObject[] ret = new OtpErlangObject[nElements];
        int i;
        for (i = 0 ; it.hasNext() ; i++) {
            ErlTerm term = it.next();
            ret[i] = erlToOtp(term);
        }
        if (i < nElements) {
            log.error("erlToOtp failed: iterator got {}, while nElem={}", i, nElements);
            throw new IllegalStateException("erlToOtp nElements mismatch");
        } else {
            return ret;
        }
    }


    public ErlTerm otpToErl(OtpErlangObject otpTerm) throws IOException {
        return otpToErl(otpTerm, null);
    }


    public ErlTerm otpToErl(OtpErlangObject otpTerm, byte[] fromBytes) throws IOException {
        // Create equivalent ErlTerm.
        Class termClass = otpTerm.getClass();
        if (termClass.equals(OtpErlangAtom.class)) {
            return ET.atom(((OtpErlangAtom) otpTerm).atomValue());
        } else if (termClass.equals(OtpErlangBinary.class)) {
            return ET.binary(((OtpErlangBinary) otpTerm).binaryValue());
        } else if (termClass.equals(OtpErlangLong.class)) {
            return ET.number(((OtpErlangLong) otpTerm).longValue());
        } else if (termClass.equals(OtpErlangDouble.class)) {
            return ET.number(((OtpErlangDouble) otpTerm).doubleValue());
        } else if (termClass.equals(OtpErlangList.class)) {
            return ET.list(otpToErl(((OtpErlangList) otpTerm).elements()));
        } else if (termClass.equals(OtpErlangRef.class)) {
            return ET.ref(fromBytes == null ? otpToBytes(otpTerm) : fromBytes);
        } else if (termClass.equals(OtpErlangString.class)) {
            return ET.list(((OtpErlangString) otpTerm).stringValue());
        } else if (termClass.equals(OtpErlangTuple.class)) {
            return ET.tuple(otpToErl(((OtpErlangTuple) otpTerm).elements()));
        } else {
            log.error("unhandled Otp term type: {}", otpTerm);
            throw new ProtocolException("unhandled OTP term type: "+ otpTerm);
        }
    }


    public ErlTerm[] otpToErl(OtpErlangObject[] otpTerms) throws IOException {
        ErlTerm[] ret = new ErlTerm[otpTerms.length];
        for (int i = 0 ; i < otpTerms.length ; i++) {
            ret[i] = otpToErl(otpTerms[i]);
        }
        return ret;
    }


    /**
     * Translate an OTP term to its external bytes representation.
     * Note that we prepend the version tag!
     * @param otpTerm   Term to serialize.
     * @return          Byte array representing the term in ETF.
     */
    public static byte[] otpToBytes(OtpErlangObject otpTerm) {
        OtpOutputStream stream = new OtpOutputStream();
        stream.write(OtpExternal.versionTag);
        stream.write_any(otpTerm);
        return stream.toByteArray();
    }


    

}
